package workflow

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/go-gorp/gorp"
	"github.com/rockbears/log"

	"github.com/ovh/cds/engine/api/application"
	"github.com/ovh/cds/engine/api/database/gorpmapping"
	"github.com/ovh/cds/engine/api/metrics"
	"github.com/ovh/cds/engine/api/repositoriesmanager"
	"github.com/ovh/cds/engine/cache"
	"github.com/ovh/cds/engine/gorpmapper"
	"github.com/ovh/cds/sdk"
)

// SaveVulnerabilityReport calculate vulnerability trend and save report.
func SaveVulnerabilityReport(ctx context.Context, db gorpmapper.SqlExecutorWithTx, cache cache.Store, proj sdk.Project, nr *sdk.WorkflowNodeRun, workerReport sdk.VulnerabilityWorkerReport) error {
	var defaultBranch string

	// Get default branch
	if nr.VCSServer != "" {
		// Get vcs info to known if we are on the default branch or not
		client, err := repositoriesmanager.AuthorizedClient(ctx, db, cache, proj.Key, nr.VCSServer)
		if err != nil {
			return sdk.NewErrorWithStack(err, sdk.WrapError(sdk.ErrNoReposManagerClientAuth, "cannot get repo client %s", nr.VCSServer))
		}

		b, err := repositoriesmanager.DefaultBranch(ctx, client, nr.VCSRepository)
		if err != nil {
			return sdk.WrapError(err, "unable to get default branch")
		}
		defaultBranch = b.DisplayID
	}

	// Get node run report if exists
	nodeRunReport, err := loadVulnerabilityReport(db, nr.ID)
	if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
		return sdk.WrapError(err, "unable to load vulnerability report")
	}
	if nodeRunReport == nil {
		nodeRunReport = &sdk.WorkflowNodeRunVulnerabilityReport{
			WorkflowID:        nr.WorkflowID,
			ApplicationID:     nr.ApplicationID,
			Branch:            nr.VCSBranch,
			Num:               nr.Number,
			WorkflowNodeRunID: nr.ID,
			WorkflowRunID:     nr.WorkflowRunID,
			Report: sdk.WorkflowNodeRunVulnerability{
				Vulnerabilities: workerReport.Vulnerabilities,
			},
		}

		// Get summary from previous run
		previousRunReport, err := loadPreviousRunVulnerabilityReport(db, nr)
		if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
			return sdk.WrapError(err, "unable to get previous vulnerability report")
		}
		nodeRunReport.Report.PreviousRunSummary = previousRunReport

		// Get summary from default branch
		if defaultBranch != "" && defaultBranch != nr.VCSBranch {
			defaultBranchReport, err := loadLatestRunVulnerabilityReport(db, nr, defaultBranch)
			if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
				return sdk.WrapError(err, "unable to get default branch vulnerability report")
			}
			nodeRunReport.Report.DefaultBranchSummary = defaultBranchReport
		}
	}

	// Append new vulnerabilities to report
	newVulnerabilities := make([]sdk.Vulnerability, 0, len(workerReport.Vulnerabilities))
	for _, v := range workerReport.Vulnerabilities {
		v.Type = workerReport.Type
		newVulnerabilities = append(newVulnerabilities, v)
	}

	// Remove existing vulnerabilities for same type in node report
	filteredVulnerabilities := make([]sdk.Vulnerability, 0, len(nodeRunReport.Report.Vulnerabilities)+len(newVulnerabilities))
	removedVulnerabilities := make([]sdk.Vulnerability, 0, len(nodeRunReport.Report.Vulnerabilities))
	for _, v := range nodeRunReport.Report.Vulnerabilities {
		if v.Type != workerReport.Type {
			filteredVulnerabilities = append(filteredVulnerabilities, v)
		} else {
			removedVulnerabilities = append(removedVulnerabilities, v)
		}
	}
	nodeRunReport.Report.Vulnerabilities = append(filteredVulnerabilities, newVulnerabilities...)

	// Init or update summary counter
	if nodeRunReport.Report.Summary == nil {
		nodeRunReport.Report.Summary = make(map[string]int64)
	}
	for i := range removedVulnerabilities {
		if _, ok := nodeRunReport.Report.Summary[removedVulnerabilities[i].Severity]; ok {
			nodeRunReport.Report.Summary[removedVulnerabilities[i].Severity]--
		}
	}
	for i := range newVulnerabilities {
		if _, ok := nodeRunReport.Report.Summary[newVulnerabilities[i].Severity]; !ok {
			nodeRunReport.Report.Summary[newVulnerabilities[i].Severity] = 0
		}
		nodeRunReport.Report.Summary[newVulnerabilities[i].Severity]++
	}

	// Load existing vulnerabilities for application to check if some are ignored
	appVulnerabilities, err := application.LoadVulnerabilities(db, nr.ApplicationID)
	if err != nil {
		return sdk.WrapError(err, "unable to load vulnerabilities for application with id %d", nr.ApplicationID)
	}

	key := func(v sdk.Vulnerability) string {
		return fmt.Sprintf("%s-%s-%s-%s", v.Type, v.Component, v.Version, v.CVE)
	}

	// Create a map of all ignored vulnerabilities
	mIgnored := make(map[string]struct{})
	for i := range appVulnerabilities {
		if appVulnerabilities[i].Ignored {
			mIgnored[key(appVulnerabilities[i])] = struct{}{}
		}
	}

	// For all report's vulnerabilities set ignored flag if true on application
	for _, v := range nodeRunReport.Report.Vulnerabilities {
		if _, ok := mIgnored[key(v)]; ok {
			v.Ignored = true
		}
	}

	if nodeRunReport.ID == 0 {
		if err := InsertVulnerabilityReport(db, nodeRunReport); err != nil {
			return sdk.WrapError(err, "unable to save vulnerability report")
		}
	} else {
		if err := UpdateVulnerabilityReport(db, nodeRunReport); err != nil {
			return sdk.WrapError(err, "unable to save vulnerability report")
		}
	}

	// If we are on default branch, save report on application
	if defaultBranch != "" && defaultBranch == nr.VCSBranch {
		if err := application.DeleteVulnerabilitiesByApplicationIDAndType(db, nr.ApplicationID, workerReport.Type); err != nil {
			return err
		}
		if err := application.InsertVulnerabilities(db, newVulnerabilities, nr.ApplicationID); err != nil {
			return err
		}

		// push metrics
		vulnsDBSummary, errS := application.LoadVulnerabilitiesSummary(db, nr.ApplicationID)
		if errS != nil {
			log.Error(ctx, "SaveVulnerabilityReport> Unable to get summary to create metrics: %s", err)
		}
		if vulnsDBSummary != nil && errS == nil {
			metrics.PushVulnerabilities(proj.Key, nr.ApplicationID, nr.WorkflowID, nr.Number, vulnsDBSummary)
		}
	}

	return nil
}

func loadPreviousRunVulnerabilityReport(db gorp.SqlExecutor, nr *sdk.WorkflowNodeRun) (map[string]int64, error) {
	var dbReport dbNodeRunVulenrabilitiesReport
	query := `
    SELECT * FROM workflow_node_run_vulnerability
    WHERE application_id = $1 AND workflow_id = $2 AND branch = $3 AND workflow_number < $4
    ORDER BY workflow_number DESC
    LIMIT 1
  `
	if err := db.SelectOne(&dbReport, query, nr.ApplicationID, nr.WorkflowID, nr.VCSBranch, nr.Number); err != nil {
		if err == sql.ErrNoRows {
			return nil, sdk.WithStack(sdk.ErrNotFound)
		}
		return nil, sdk.WrapError(err, "unable to load previous report")
	}
	return dbReport.Report.Summary, nil
}

func loadLatestRunVulnerabilityReport(db gorp.SqlExecutor, nr *sdk.WorkflowNodeRun, branch string) (map[string]int64, error) {
	var dbReport dbNodeRunVulenrabilitiesReport
	query := `
    SELECT * FROM workflow_node_run_vulnerability
    WHERE application_id = $1 AND workflow_id = $2 AND branch = $3
    ORDER BY workflow_number DESC, workflow_node_run_id DESC
    LIMIT 1
  `
	if err := db.SelectOne(&dbReport, query, nr.ApplicationID, nr.WorkflowID, branch); err != nil {
		if err == sql.ErrNoRows {
			return nil, sdk.WithStack(sdk.ErrNotFound)
		}
		return nil, sdk.WrapError(err, "Unable to load latest report")
	}
	return dbReport.Report.Summary, nil
}

// InsertVulnerabilityReport in database.
func InsertVulnerabilityReport(db gorp.SqlExecutor, report *sdk.WorkflowNodeRunVulnerabilityReport) error {
	dbReport := dbNodeRunVulenrabilitiesReport(*report)
	if err := db.Insert(&dbReport); err != nil {
		return sdk.WrapError(err, "unable to insert report")
	}
	*report = sdk.WorkflowNodeRunVulnerabilityReport(dbReport)
	return nil
}

// UpdateVulnerabilityReport in database.
func UpdateVulnerabilityReport(db gorp.SqlExecutor, report *sdk.WorkflowNodeRunVulnerabilityReport) error {
	dbReport := dbNodeRunVulenrabilitiesReport(*report)
	if _, err := db.Update(&dbReport); err != nil {
		return sdk.WrapError(err, "upable to update report with id %d", report.ID)
	}
	*report = sdk.WorkflowNodeRunVulnerabilityReport(dbReport)
	return nil
}

// PostGet  is a db hook
func (d *dbNodeRunVulenrabilitiesReport) PostGet(db gorp.SqlExecutor) error {
	var reportS sql.NullString
	query := "SELECT report from workflow_node_run_vulnerability WHERE id = $1"
	if err := db.QueryRow(query, d.ID).Scan(&reportS); err != nil {
		return sdk.WrapError(err, "Unable to report")
	}

	var report sdk.WorkflowNodeRunVulnerability
	if err := gorpmapping.JSONNullString(reportS, &report); err != nil {
		return sdk.WrapError(err, "Unable to unmarshal report")
	}

	d.Report = report
	return nil
}

// PostInsert  is a db hook
func (d *dbNodeRunVulenrabilitiesReport) PostInsert(db gorp.SqlExecutor) error {
	report, err := gorpmapping.JSONToNullString(d.Report)
	if err != nil {
		return sdk.WrapError(err, "unable to marshal report")
	}
	query := "UPDATE workflow_node_run_vulnerability set report=$1 WHERE id=$2"
	if _, err := db.Exec(query, report, d.ID); err != nil {
		return sdk.WrapError(err, "unable to insert report")
	}
	return nil
}

func (d *dbNodeRunVulenrabilitiesReport) PostUpdate(db gorp.SqlExecutor) error {
	return d.PostInsert(db)
}

func loadVulnerabilityReport(db gorp.SqlExecutor, nodeRunID int64) (*sdk.WorkflowNodeRunVulnerabilityReport, error) {
	var dbReport dbNodeRunVulenrabilitiesReport
	query := `
    SELECT * FROM workflow_node_run_vulnerability
		WHERE workflow_node_run_id = $1
		ORDER BY id DESC
		LIMIT 1
  `
	if err := db.SelectOne(&dbReport, query, nodeRunID); err != nil {
		if err == sql.ErrNoRows {
			return nil, sdk.WithStack(sdk.ErrNotFound)
		}
		return nil, sdk.WrapError(err, "unable to load vulnerability report for node run %d", nodeRunID)
	}
	report := sdk.WorkflowNodeRunVulnerabilityReport(dbReport)
	return &report, nil
}
